Skip to content

fix(bench): give in-process its own drain byte cap - #179

Merged
zannis merged 1 commit into
mainfrom
zannis/fix/raise-large-payload-drain-corpus
Sep 9, 2026
Merged

fix(bench): give in-process its own drain byte cap#179
zannis merged 1 commit into
mainfrom
zannis/fix/raise-large-payload-drain-corpus

Conversation

@zannis

@zannis zannis commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Why

benches/results/bench-results.json on main holds six runs and 822 rows, and 99 of those rows are setup_bound — measured, but withheld from every chart. The largest single block is the in-process 64 KiB consume leg: at the matrix's 3 GiB cap that leg is 49 152 messages, and the 2026-09-08 run drained every one of the eleven cells in 0.09–0.46 s. All eleven fall under MIN_FRAMEWORK_WINDOW_SECS, so the harness marks them setup_bound and the charts withhold all eleven behind one caption line:

inmemory / parallel, batch: measured, but every window under 1 s — too short to publish as a rate, even a lower bound

Nothing false is published, but the fastest backend in the set contributes no 64 KiB consume rate at all.

backend 64 KiB drain rows publishable (framework) withheld (setup_bound)
inmemory 11 0 11
kafka 12 5 7
nats 12 12 0
rabbitmq 9 9 0
redis 12 12 0
sqs 8 8 0

Why not just raise the cap

Because 3 GiB is not a cautious number for the other five — it is the containerised limit. Every other backend stages its corpus inside the 8 GB Docker VM (Redis and NATS in container memory, Kafka through its page cache), where 3 GiB is already over a third of the VM and where a 3.2 GB backlog has already taken Redis down mid-pass — the reason --load-backlog-max-bytes exists at all.

A global raise also lands on a leg that does not need it: past ~5.7 GiB the 1 KiB leg stops being byte-bound and doubles to the pinned 6 M for every backend, which projects to about +45 min of drain wall clock (RabbitMQ's 1 KiB leg alone goes 25.8 → 49.1 min) to fix rows that already clear the floor.

In-process is the one backend that stages its corpus in the harness process on the 64 GB host and never starts a container. So its cap is the one that can rise.

What this does

Substitutes --drain-max-bytes for the inmemory target only, at 32 GiB — the mirror image of the SQS corpus deviation already in the script. SQS drains a smaller corpus because it is the only backend slow enough that the pinned count means something else there; in-process is allowed a larger one because it is the only backend fast enough that the pinned byte cap does. Both substitutions now go through one substitute_knob helper that still dies when its knob leaves the matrix.

32 GiB is 524 288 messages at 64 KiB. Projecting the measured rates onto that corpus:

flow / consumers window marker
consume_parallel 1/2/4/8 1.32 / 1.22 / 1.20 / 1.43 s framework
consume_batch 1/2/4 4.96 / 2.83 / 2.40 s framework
consumer_group 1/4/8 1.34 / 1.11 / 1.33 s framework
consumer_group 2 0.92 s still setup_bound

Ten of eleven publish. The eleventh is the fastest cell in the document (514 k msg/s); clearing it too needs ~40 GiB, and MIN_FRAMEWORK_CORPUS_MESSAGES's own doc declines to chase a window that hardware speed keeps moving. It stays withheld and captioned, which costs one bar rather than the pass.

It moves the 1 KiB leg too, by design: 3 145 728 → the pinned 6 000 000, so in-process runs two of its three legs on the matrix's own corpus rather than one, and the single 1 KiB cell that drained in 0.97 s clears the floor. Cost: ~40 s and 6 GiB rather than 3 GiB resident. Peak resident is the 64 KiB leg at ~34 GB of 64 GB, with no container to share the host.

Verification

Argv, per backend — stubbed cargo/docker, ran the script:

  • inmemory--drain-messages 6000000 --drain-max-bytes 34359738368
  • kafka, redis → both pinned values untouched
  • sqs--drain-messages 60000, byte cap untouched at 3 GiB, --fifo-messages 100

The guard fires — removed --drain-max-bytes from MATRIX and re-ran inmemory:

bench.sh: the matrix has no --drain-max-bytes for the inmemory deviation to replace

Refusal gate clearedrefused_drain_capacity would refuse a corpus above the backend's declared queue capacity. QUEUE_CAPACITY is 8 000 000 and is a bound rather than a preallocation, so 524 288 clears it. The byte cap can only ever lower the corpus below --drain-messages, so 8 M bounds any cap value.

Rendering control — projected that corpus onto the committed six-backend document (respecting the validator's drain derivations) and ran chartgen. It renders every family at exit 0. The caption drops the "every window under 1 s" note and gains a two-line group:

corpus differs by backend: inmemory 524k; kafka, nats, rabbitmq, redis, sqs 49k — a smaller corpus is a shorter window, not a different measurement

The parallel-vs-sequenced-64kib canvas goes 672 → 688 of the 768 that MAX_CAPTION_GROWTH_LINES (8) allows, so three of eight growth lines are spent and five stay spare. Every other family stays on the base 640. This is the "wider corpus deviation" #178's budget test anticipated, and it seats inside the allowance.

Suitecargo nextest run --no-default-features -E 'binary(chartgen)'128 tests run, 128 passed, 0 skipped. No document or SVG churn: the committed document is unchanged until the rerun, so the byte-compare has nothing to disagree with.

What this does not fix

Of the four cells that failed "consumed before assembly" at 64 KiB / 8 consumers, in-process was one and this clears it (unique_at_start 10 500 of 524 288, 2%). The other three are RabbitMQ, whose eight-consumer group assembly ran through 37 000–46 300 messages. Putting that well under half a corpus needs ~196 k messages — 12 GiB inside an 8 GB VM. Those three are a group-assembly cost, not a corpus size, and they are expected to fail again. Recorded in the script and the runbook so the rerun is not read as having fixed them.

Kafka's seven setup_bound 64 KiB rows are the same shape: they would need ~196 k messages too, which its page cache would have to hold inside the same VM.

Follow-up

This is the matrix change for CAF-889 T7. The rerun itself is on the pinned host (M4 Max, per the CAF-665 host decision), so it lands separately — scripts/bench.sh per backend in the pinned order, then scripts/bench.sh charts. Redis's consume_fifo gap closes in that same pass via #172, per the decision to leave it as published until a full rerun.

Depends-on: none — script and runbook only; the byte-cap split it builds on merged as #177, and the chart-budget headroom as #178.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 9, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
shove-docs cc8923c Commit Preview URL

Branch Preview URL
Sep 09 2026, 02:16 PM

At the matrix's 3 GiB the 64 KiB leg is 49 152 messages, and the 2026-09-08
run drained every one of the in-process 64 KiB cells in 0.09-0.46 s. That is
under MIN_FRAMEWORK_WINDOW_SECS, so the harness marked all eleven rows
setup_bound and the charts withheld all eleven and captioned why: the fastest
backend in the set publishes no 64 KiB consume rate at all, and 99 of the
document's 822 rows are setup_bound.

3 GiB is not a cautious number for the other five, it is the containerised
limit -- they stage the corpus inside the 8 GB Docker VM, where 3 GiB is
already over a third of it and where a 3.2 GB backlog has already taken Redis
down mid-pass. Raising the cap globally would buy the 64 KiB leg at the price
of the VM. In-process stages its corpus in the harness process on the 64 GB
host and never starts a container, so it is the one backend whose cap can
rise without touching the VM.

Substitute --drain-max-bytes for the inmemory target only, at 32 GiB, the
mirror of the SQS corpus deviation already here: SQS drains a smaller corpus
because it is the only backend slow enough that the pinned count means
something else, in-process a larger one because it is the only one fast enough
that the pinned byte cap does. Factor both substitutions through
substitute_knob so a deviation still dies when its knob leaves the matrix.

32 GiB is 524 288 messages at 64 KiB. Projecting the measured rates onto that
corpus puts ten of the eleven cells at 1.0-5.0 s and the eleventh
(consumer_group at two consumers, 514 k msg/s) at ~0.9 s, so the leg publishes
instead of being withheld whole; a cell under the floor is still withheld and
captioned. It also puts the 1 KiB leg back on the pinned 6 000 000 from
3 145 728, clearing the one cell that drained in 0.97 s.

Rendering control: projecting that corpus onto the committed six-backend
document and running chartgen drops the "inmemory / parallel, batch: measured,
but every window under 1 s" note and adds a two-line "corpus differs by
backend: inmemory 524k; kafka, nats, rabbitmq, redis, sqs 49k" group. The
64 KiB parallel-vs-sequenced canvas goes 672 -> 688 of the 768 that
MAX_CAPTION_GROWTH_LINES allows, so three of eight growth lines are spent and
five stay spare.

Script and runbook only; no document or SVG churn until the rerun.
@zannis
zannis force-pushed the zannis/fix/raise-large-payload-drain-corpus branch from 7ae6d0d to cc8923c Compare September 9, 2026 14:14
@zannis
zannis merged commit cc0d1df into main Sep 9, 2026
6 checks passed
zannis added a commit that referenced this pull request Sep 10, 2026
…ne the cap would produce (#196)

Three prose claims outran the committed results document. No run, chart or
results-document change here; the byte-compare in tests/chartgen.rs is
unaffected.

The 32 GiB in-process drain byte cap (#179) landed after the in-process leg in
benches/results/bench-results.json was measured, and a leg only changes by
being re-measured. The runbook nevertheless described the cap's effects in the
present tense - "the leg publishes instead of being withheld whole", "32 GiB
puts it back on the pinned count of 6 000 000" - which reads as a description
of what is plotted. What is plotted is the 3 GiB shape: drain.corpus 49 152 at
64 KiB with all eleven rows setup_bound and withheld, and 3 145 728 at 1 KiB,
which is what the chart captions name. Those effects are now stated as what the
next in-process pass produces, with the published shape named alongside.

The same section illustrated the corpus caption with "inmemory 524k", a string
that appears in no committed chart. It now quotes the caption the committed
charts actually carry.

The "consumed before assembly" note counted four such failures with three of
them RabbitMQ. The document carries two: in-process consume_batch (37 000 of
49 152) and RabbitMQ consume_parallel (46 467 of 49 152), both at 64 KiB with
eight consumers. Corrected in the runbook and in the bench.sh comment it is
derived from.

The performance page's batching paragraph mixed an off-matrix batch-size probe
(500/50/10-message flushes at 55k/80k/79k msg/s) with a document row (~356k for
the parallel consumer) as if the four were one measurement, and quoted a number
the charts withhold without saying so. Each side is now attributed, and the
"below about 1 KiB the batch path leads" claim is replaced by what the document
holds across its own payload axis: batch ahead 3-10x at 64 B and 2-7x at 1 KiB,
behind 2-4x at 64 KiB.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant